Skip to content

Conversation

@TkDodo
Copy link
Collaborator

@TkDodo TkDodo commented Nov 11, 2025

Right now, every instance of <SentryDocumentTitle> computes a title, applies it to the document.title and stores its value in context, so that the next <SentryDocumentTitle> can pick that up and “revert” to that state.

However, there are situations where this doesn’t work so well because the unmount effect that attempts to clean things up might run after the next component has rendered, leading to wrong titles in some situations where the title just resets to Sentry:

Screen.Recording.2025-11-11.at.11.17.57.mov

The reason why it mostly works is because we have additional re-renders thanks to useLocation() at the top of most pages, which will restore the correct title. But this is brittle and as shown, doesn’t work in all cases.

This PR re-writes takes a different approach to applying document titles. Instead of writing to the title on each level in render (which can also lead to flickering), we just register with a global DocumentTitleManager in an effect. To make sure we have the right order, we compute a timestamp once during render (in useState) when the component mounts and pass that along the text we want to the manager.

The DocumentTitleManager itself then just knows all the registered titles, and displays the one we want (the last one, and optionally add the default title to it. This also gives us the possibility to do “compound titles” like in breadcrumbs if we want to). To get the last item, it uses the insertion order that was calculated during render, so the fact that useEffect runs bottom-up doesn’t matter.

Note that we can’t just reverse() the array because data fetching might mean some effects run sooner than others. For example, let’s say we add two titles, then fetch, then add two more. rendering goes like this:

1 -- 2 -- ...fetching... -- 3 -- 4

but effects will run like this (bottom up, for reach committed group individually):

2 -- 1 -- ...fetching... -- 4 -- 3

yielding a result order of 2,1,3,4, which isn’t reversible. The timestamp computed on mount resolves this issue, because we have something stable to sort by.

For cleanup, when a component unmounts, it just removes itself (by id) from the manager, which will automatically lead to the “previously registered” title to show up. This can go over an arbitrary number of layers, while the previous solution was limited to one parent.


after:

Screen.Recording.2025-11-11.at.15.17.48.mov

@github-actions github-actions bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Nov 11, 2025
@TkDodo TkDodo marked this pull request as ready for review November 11, 2025 14:22
@TkDodo TkDodo merged commit 2900817 into master Nov 12, 2025
48 checks passed
@TkDodo TkDodo deleted the tkdodo/fix/sentry-document-title branch November 12, 2025 10:50
@sentry
Copy link

sentry bot commented Nov 12, 2025

Issues attributed to commits in this pull request

This pull request was merged and Sentry observed the following issues:

Jesse-Box pushed a commit that referenced this pull request Nov 12, 2025
…103150)

Right now, every instance of `<SentryDocumentTitle>` computes a title,
applies it to the `document.title` and stores its value in context, so
that the next `<SentryDocumentTitle>` can pick that up and “revert” to
that state.

However, there are situations where this doesn’t work so well because
the unmount effect that attempts to clean things up might run after the
next component has rendered, leading to wrong titles in some situations
where the title just resets to `Sentry`:


https://github.com/user-attachments/assets/016c4e36-16d0-4a86-86fc-b27f7800db22

The reason why it mostly works is because we have additional re-renders
thanks to `useLocation()` at the top of most pages, which will restore
the correct title. But this is brittle and as shown, doesn’t work in all
cases.

This PR re-writes takes a different approach to applying document
titles. Instead of writing to the title on each level in render (which
can also lead to flickering), we just register with a global
`DocumentTitleManager` in an effect. To make sure we have the right
order, we compute a timestamp once during render (in `useState`) when
the component mounts and pass that along the text we want to the
manager.

The `DocumentTitleManager` itself then just knows all the registered
titles, and displays the one we want (the last one, and optionally add
the default title to it. This also gives us the possibility to do
“compound titles” like in breadcrumbs if we want to). To get the last
item, it uses the insertion order that was calculated during render, so
the fact that `useEffect` runs bottom-up doesn’t matter.

Note that we can’t just `reverse()` the array because data fetching
might mean some effects run sooner than others. For example, let’s say
we add two titles, then fetch, then add two more. rendering goes like
this:

```
1 -- 2 -- ...fetching... -- 3 -- 4
```

but effects will run like this (bottom up, for reach committed group
individually):

```
2 -- 1 -- ...fetching... -- 4 -- 3
```

yielding a result order of `2,1,3,4`, which isn’t reversible. The
timestamp computed on mount resolves this issue, because we have
something stable to sort by.

For cleanup, when a component unmounts, it just removes itself (by id)
from the manager, which will automatically lead to the “previously
registered” title to show up. This can go over an arbitrary number of
layers, while the previous solution was limited to one parent.

---

after:


https://github.com/user-attachments/assets/a278fc4a-7753-4ce3-a8ca-a4b9a2a9ae58
andrewshie-sentry pushed a commit that referenced this pull request Nov 13, 2025
…103150)

Right now, every instance of `<SentryDocumentTitle>` computes a title,
applies it to the `document.title` and stores its value in context, so
that the next `<SentryDocumentTitle>` can pick that up and “revert” to
that state.

However, there are situations where this doesn’t work so well because
the unmount effect that attempts to clean things up might run after the
next component has rendered, leading to wrong titles in some situations
where the title just resets to `Sentry`:


https://github.com/user-attachments/assets/016c4e36-16d0-4a86-86fc-b27f7800db22

The reason why it mostly works is because we have additional re-renders
thanks to `useLocation()` at the top of most pages, which will restore
the correct title. But this is brittle and as shown, doesn’t work in all
cases.

This PR re-writes takes a different approach to applying document
titles. Instead of writing to the title on each level in render (which
can also lead to flickering), we just register with a global
`DocumentTitleManager` in an effect. To make sure we have the right
order, we compute a timestamp once during render (in `useState`) when
the component mounts and pass that along the text we want to the
manager.

The `DocumentTitleManager` itself then just knows all the registered
titles, and displays the one we want (the last one, and optionally add
the default title to it. This also gives us the possibility to do
“compound titles” like in breadcrumbs if we want to). To get the last
item, it uses the insertion order that was calculated during render, so
the fact that `useEffect` runs bottom-up doesn’t matter.

Note that we can’t just `reverse()` the array because data fetching
might mean some effects run sooner than others. For example, let’s say
we add two titles, then fetch, then add two more. rendering goes like
this:

```
1 -- 2 -- ...fetching... -- 3 -- 4
```

but effects will run like this (bottom up, for reach committed group
individually):

```
2 -- 1 -- ...fetching... -- 4 -- 3
```

yielding a result order of `2,1,3,4`, which isn’t reversible. The
timestamp computed on mount resolves this issue, because we have
something stable to sort by.

For cleanup, when a component unmounts, it just removes itself (by id)
from the manager, which will automatically lead to the “previously
registered” title to show up. This can go over an arbitrary number of
layers, while the previous solution was limited to one parent.

---

after:


https://github.com/user-attachments/assets/a278fc4a-7753-4ce3-a8ca-a4b9a2a9ae58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants